using System;
using System.Net;
using System.Net.Sockets;
using System.Reflection;
using System.Threading;

using csammisrun.OscarLib;

namespace csammisrun.OscarLib.Utility
{
  internal delegate void ServerConnectionCompletedHandler(Connection conn);
  internal delegate void ConnectionReadPacketHandler(Connection conn, byte[] data);
  
  /// <summary>
  /// Encapsulates a connection to an OSCAR server
  /// </summary>
  internal class Connection
  {
    #region Static socket factory methods
    /// <summary>
    /// Encapsulates socket connection information
    /// </summary>
    class SocketConnectionData
    {
      public string Server;
      public int Port;
      public Socket Socket;
      public Delegate Callback;
    }

    /// <summary>
    /// Begins the socket creation process
    /// </summary>
    public static void CreateDirectConnectSocket(string host, int port, Delegate callback)
    {
		// Make sure the callback is the right type
		// TODO: Better error prevention for CF?
#if !WindowsCE
      ParameterInfo[] param = callback.Method.GetParameters();
      if (param.Length != 2 || param[0].ParameterType != typeof(Socket) || param[1].ParameterType != typeof(string))
      {
        throw new ArgumentException("Callback delegate must take a Socket and a string as its parameters");
      }
#endif

		SocketConnectionData scd = new SocketConnectionData();
      scd.Callback = callback;
      scd.Port = port;
      scd.Server = host;
      scd.Socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

      try
      {
        Dns.BeginGetHostEntry(scd.Server, new AsyncCallback(CreateDCSEndDnsLookup), scd);
      }
      catch (SocketException sockex)
      {
        string message = "Cannot connect to DNS service:"
			+ Environ.NewLine + sockex.Message;

#if WindowsCE
		ProxiedSocketFactoryResultHandler handler = (callback as ProxiedSocketFactoryResultHandler);
		handler(scd.Socket, message);
#else
        callback.DynamicInvoke(scd.Socket, message);
#endif
	}
    }
    /// <summary>
    /// Ends the DNS lookup phase of connection and begins connecting the socket to the host
    /// </summary>
    protected static void CreateDCSEndDnsLookup(IAsyncResult res)
    {
      SocketConnectionData scd = null;

      IPHostEntry hosts = null;
      try
      {
        scd = res.AsyncState as SocketConnectionData;
        hosts = Dns.EndGetHostEntry(res);
      }
      catch (Exception sockex)
      {
        string message = "Cannot resolve server:"
		  + Environ.NewLine + sockex.Message;

#if WindowsCE
		ProxiedSocketFactoryResultHandler handler = (scd.Callback as ProxiedSocketFactoryResultHandler);
		handler(scd.Socket, message);
#else
        scd.Callback.DynamicInvoke(scd.Socket, message);
#endif
        return;
      }

      IPAddress address = hosts.AddressList[0];
      IPEndPoint ipep = new IPEndPoint(address, scd.Port);

      try
      {
        scd.Socket.BeginConnect(ipep, new AsyncCallback(CreateDCSEndInitialConnection), scd);
      }
      catch (SocketException sockex)
      {
        string message = "Cannot connect to server:"
          + Environ.NewLine + sockex.Message;

#if WindowsCE
		ProxiedSocketFactoryResultHandler handler = (scd.Callback as ProxiedSocketFactoryResultHandler);
		handler(scd.Socket, message);
#else
        scd.Callback.DynamicInvoke(scd.Socket, message);
#endif
      }
    }

    /// <summary>
    /// Ends the connection phase and returns a connected socket
    /// </summary>
    protected static void CreateDCSEndInitialConnection(IAsyncResult res)
    {
      SocketConnectionData scd = null;
      try
      {
        scd = res.AsyncState as SocketConnectionData;
        scd.Socket.EndConnect(res);
#if WindowsCE
		ProxiedSocketFactoryResultHandler handler = (scd.Callback as ProxiedSocketFactoryResultHandler);
		handler(scd.Socket, "");
#else
        scd.Callback.DynamicInvoke(scd.Socket, "");
#endif
      }
      catch (Exception sockex)
      {
        string message = "Can't connect to server: "
          + Environ.NewLine + sockex.Message;

#if WindowsCE
		ProxiedSocketFactoryResultHandler handler = (scd.Callback as ProxiedSocketFactoryResultHandler);
		handler(scd.Socket, message);
#else
        scd.Callback.DynamicInvoke(scd.Socket, message);
#endif
      }
    }
    #endregion

    public event ServerConnectionCompletedHandler ServerConnnectionCompleted;
    public event ConnectionReadPacketHandler ReadPacketAsyncComplete;

    protected int _id = -1;
    ushort _flapsequencenum = 1;

    protected Socket _sock = null;
    protected bool _disconnecting = false;
    protected bool _connecting = false;
    protected bool _readyfordata = false;
    protected Session _parent = null;
    protected string _server = "";
    protected int _port = 0;
    protected byte[] _cookie = null;

    AsyncCallback _receivecallback = null;
    Timer _keepalivetimer = null;
    TimerCallback _keepalivecallback = null;
    ProcessQueue _processor = new ProcessQueue();

    /// <summary>
    /// Creates a new connection
    /// </summary>
    /// <param name="parent">The <see cref="Session"/> that owns this connection</param>
    /// <param name="id">The connection's unique ID</param>
    public Connection(Session parent, int id)
    {
      _receivecallback = new AsyncCallback(ProcessFLAP);
      _keepalivecallback = new TimerCallback(SendKeepalive);
      _keepalivetimer = new Timer(_keepalivecallback, null, Timeout.Infinite, Timeout.Infinite);
      _parent = parent;
      _id = id;
    }

    /// <summary>
    /// Connects to the server and performs the initial handshaking
    /// </summary>
    public virtual bool ConnectToServer()
    {
      _parent.ProxiedSocketFactory(_server, _port, new ProxiedSocketFactoryResultHandler(ProxiedSocketFactoryResult));
      return true;
    }

    #region Initial connection async callbacks
    void ProxiedSocketFactoryResult(Socket sock, string errormsg)
    {
      if (!String.IsNullOrEmpty(errormsg))
      {
        Logging.WriteString(errormsg + ", connection " + _id.ToString());
        this.DisconnectFromServer(true);
        return;
      }

      _sock = sock;
      this.Connecting = true;

      // Read in the first ten bytes (6 byte FLAP channel 0x01 + 0x00000001)
      // of the connection handshake
      byte[] serverhandshake = new byte[10];
      try
      {
        int bytesreceived = 0;
        int receiveindex = 0;
        lock (_sock)
        {
          while (bytesreceived < 10)
          {
            bytesreceived = _sock.Receive(serverhandshake, receiveindex, 10 - receiveindex, SocketFlags.None);
            receiveindex += bytesreceived;
          }
        }
      }
      catch (SocketException sockex)
      {
        string message = "Can't read handshake from server: "
          + Environ.NewLine + sockex.Message;
        Logging.WriteString(message + ", connection " + _id.ToString());
        this.DisconnectFromServer(true);
        return;
      }
      finally
      {
        serverhandshake = null;
      }

      // Construct our reply to the connection handshake
      FLAPHeader fh;
      fh.Channel = 0x01;
      fh.DatagramSequenceNumber = this.FLAPSequence;
      fh.DataSize = (ushort)(4 + ((_cookie == null) ? 0 : 4 + _cookie.Length));

      int index = 0;
      byte[] clienthandshake = new byte[fh.DataSize + 6];
      Marshal.InsertFLAPHeader(fh, clienthandshake, ref index);
      Marshal.InsertUint(clienthandshake, Constants.PROTOCOL_VERSION, ref index);
      if (_cookie != null)
      {
        Marshal.InsertTLVs(
          new TLV[] { TLVMarshal.MakeTLV(0x0006, _cookie) },
          clienthandshake, ref index);
      }
      try
      {
        lock (_sock)
        {
          _sock.Send(clienthandshake);
        }
      }
      catch (SocketException sockex)
      {
        string message = "Couldn't send handshake to server:"
          + Environ.NewLine + sockex.Message;
        Logging.WriteString(message + ", connection " + _id.ToString());
        this.DisconnectFromServer(true);
        return;
      }
      finally
      {
        clienthandshake = null;
      }

      this.Connecting = true;

      // And the handshaking is done. Auth connection will send
      // SNAC(17,06) and any other connection will receive SNAC(01,03)
      OnServerConnectionCompleted();
    }
    #endregion

    /// <summary>
    /// Disconnects from the OSCAR server
    /// </summary>
    /// <param name="error"><c>true</c> if the disconnection is resulting from an error, <c>false</c> otherwise</param>
    /// <returns><c>true</c> if the disconnection succeeded without error,
    /// <c>false</c> otherwise</returns>
    public bool DisconnectFromServer(bool error)
    {
      _disconnecting = true;

      try
      {
        _sock.Blocking = false;
        _sock.Shutdown(SocketShutdown.Both);
        _sock.Close();
      }
      catch (Exception ex)
      {
        string message = "don't care: " + ex.Message;
        return false;
      }
      finally
      {
        _parent.Connections.DeregisterConnection(this, error);
        _connecting = false;
        _keepalivetimer.Change(Timeout.Infinite, Timeout.Infinite);
      }
      return true;
    }

    #region FLAP sending and receiving
    /// <summary>
    /// Gets the next FLAP header from the server
    /// </summary>
    public virtual void ReadHeader()
    {
      try
      {
        byte[] flapbuffer = new byte[6];
        lock (_sock)
        {
          _sock.BeginReceive(flapbuffer, 0, flapbuffer.Length, SocketFlags.None,
            _receivecallback, flapbuffer);
        }
      }
      catch (Exception ex)
      {
        if (!_disconnecting)
        {
          string message = "Receive error: " + ex.Message;
          Logging.WriteString(message + ", connection " + _id.ToString());
          this.DisconnectFromServer(true);
        }
      }
    }

    /// <summary>
    /// Processes the asynchronous receipt of a FLAP
    /// </summary>
    /// <param name="res">The <see cref="IAsyncResult"/> of a BeginReceive call</param>
    private void ProcessFLAP(IAsyncResult res)
    {
      int bytesreceived = 0;
      int receiveindex = 0;

      byte[] flapbuffer = null;

      try
      {
        lock (_sock)
        {
          bytesreceived = _sock.EndReceive(res);
          if (bytesreceived == 0)
          {
            throw new Exception("Socket receive returned 0 bytes read");
          }

          flapbuffer = (byte[])res.AsyncState;

          receiveindex = bytesreceived;
          while (receiveindex < flapbuffer.Length)
          {
            bytesreceived = _sock.Receive(flapbuffer,
              receiveindex,
              flapbuffer.Length - receiveindex,
              SocketFlags.None);
            if (bytesreceived == 0)
            {
              throw new Exception("Socket receive returned 0 bytes read");
            }
            receiveindex += bytesreceived;
          }
        }
      }
      catch (Exception ex)
      {
        if (!_disconnecting)
        {
          string message = "Receive error: " + ex.Message;
          Logging.WriteString(message + ", connection " + _id.ToString());
          this.DisconnectFromServer(true);
        }
        return;
      }

      if (flapbuffer[0] == 0xFF)
      {
        int badcount = 0;
        for(badcount = 0; badcount < flapbuffer.Length && flapbuffer[badcount] == 0xFF; badcount++);

        // SOMEHOW there are two bytes of 0xFF occuring when requesting
        // SNAC family 0x10 and receiving SNAC(01,03).  So that has to stop
        for (int i = badcount; i < flapbuffer.Length; i++)
        {
          flapbuffer[i - badcount] = flapbuffer[i];
        }

        _sock.Receive(flapbuffer, flapbuffer.Length - badcount, badcount, SocketFlags.None);
      }

      // Get the FLAP header out of the async result
      FLAPHeader flap = Marshal.GetFLAPHeader(flapbuffer);
      byte[] packet = ReadPacket(flap.DataSize);
      if (packet == null)
      {
        return;
      }

      // The full packet is here, so we can chuck it out for processing
      switch (flap.Channel)
      {
        case 0x01:	// New connection negotiation
          // This will not occur, FLAP 0x01 is handled in ConnectToServer
          break;
        case 0x02:	//  SNAC data
          if (packet.Length < 10)
          {
            Logging.DumpFLAP(packet, "Receive error: Got a SNAC without a full header on connection" + _id.ToString());
            break; // Don't return, don't disconnect, just keep on keeping on
          }

          // Break up the SNAC header and data,
          // and send the packet to the dispatch queue
          DataPacket dp = new DataPacket();
          dp.FLAP = flap;
          dp.ParentConnection = this;
          dp.ParentSession = _parent;
          SNACHeader sh;
          dp.Data = Marshal.GetSNACData(packet, out sh);
          dp.SNAC = sh;

          this.Processor.Enqueue(dp);
          break;
        case 0x03:	// FLAP error
          // Session error:  FLAP error, bailing out
          Logging.WriteString("Received error FLAP");
          break;
        case 0x04:	// Close connection negotiation
          Logging.WriteString("Received close connection FLAP");
          DisconnectFromServer(false);
          return;
        case 0x05:	// Keepalive packet
          Logging.WriteString("Received keepalive FLAP");
          SendKeepalive(null);
          break;
        default:
          break;
      }

      // Shine on, you crazy connection
      _keepalivetimer.Change(60000, Timeout.Infinite);
      ReadHeader();
    }

    /// <summary>
    /// Synchronously read in a packet
    /// </summary>
    public byte[] ReadPacket(int datalength)
    {
      byte[] packet = new byte[datalength];
      int bytesreceived = 0;
      int receiveindex = 0;

      try
      {
        lock (_sock)
        {
          while (receiveindex < packet.Length)
          {
            bytesreceived = _sock.Receive(
              packet,
              receiveindex,
              packet.Length - receiveindex,
              SocketFlags.None);
            if (bytesreceived == 0)
            {
              throw new Exception("Socket receive returned 0 bytes read");
            }
            receiveindex += bytesreceived;
          }
        }
      }
      catch (Exception ex)
      {
        if (!_disconnecting)
        {
          string message = "Receive error: " + ex.Message;
          Logging.WriteString(message + ", connection " + _id.ToString());
          this.DisconnectFromServer(true);
          return null;
        }
      }

      return packet;
    }

    public void ReadPacketAsync(int datalength)
    {
      byte[] buffer = new byte[datalength];
      try
      {
        lock (_sock)
        {
          _sock.BeginReceive(buffer, 0, buffer.Length, SocketFlags.None, new AsyncCallback(EndReadPacketAsync), buffer);
        }
      }
      catch (Exception ex)
      {
        if (!_disconnecting)
        {
          string message = "Receive error: " + ex.Message;
          Logging.WriteString(message + ", connection " + _id.ToString());
          this.DisconnectFromServer(true);
        }
      }
    }

    private void EndReadPacketAsync(IAsyncResult res)
    {
      byte[] buffer = null;
      int bytesreceived = 0;
      int receiveindex = 0;
      try
      {
        lock (_sock)
        {
          bytesreceived = _sock.EndReceive(res);
          if (bytesreceived == 0)
          {
            throw new Exception("Socket receive returned 0 bytes read");
          }

          buffer = (byte[])res.AsyncState;

          receiveindex = bytesreceived;
          while (receiveindex < buffer.Length)
          {
            bytesreceived = _sock.Receive(buffer, receiveindex, buffer.Length - receiveindex, SocketFlags.None);
            if (bytesreceived == 0)
            {
              throw new Exception("Socket receive returned 0 bytes read");
            }
            receiveindex += bytesreceived;
          }
        }

        if (this.ReadPacketAsyncComplete != null)
        {
          this.ReadPacketAsyncComplete(this, buffer);
        }
      }
      catch (Exception ex)
      {
        if (!_disconnecting)
        {
          string message = "Receive error: " + ex.Message;
          Logging.WriteString(message + ", connection " + _id.ToString());
          this.DisconnectFromServer(true);
        }
      }
    }

    /// <summary>
    /// Sends a single FLAP
    /// </summary>
    /// <param name="buffer">The byte buffer to send</param>
    public virtual void SendFLAP(byte[] buffer)
    {
      int sentbytes = 0, sendindex = 0;

      try
      {
        lock (_sock)
        {
          // The FLAP ID gets assigned here so we can guaruntee per-connection sequence
          Marshal.InsertUshort(buffer, this.FLAPSequence, 2);
          while (sendindex < buffer.Length)
          {
            sentbytes = _sock.Send(buffer, sendindex, buffer.Length - sendindex, SocketFlags.None);
            if (sentbytes == 0)
            {
              throw new Exception("Socket send returned 0 bytes transmitted");
            }
            sendindex += sentbytes;
          }
          _keepalivetimer.Change(60000, Timeout.Infinite);

          if (buffer[1] == 0x02)
          {
            ushort fam = Marshal.ByteArrayToUshort(buffer, 6);
            ushort sub = Marshal.ByteArrayToUshort(buffer, 8);
            Logging.DumpFLAP(buffer, String.Format("Connection {3} sent SNAC({0:x2},{1:x2}), {2} bytes",
              fam, sub, sentbytes, this.ID));
          }
        }
      }
      catch (Exception ex)
      {
        if (!_disconnecting)
        {
          string message = "Send error: " + ex.Message;
          Logging.WriteString(message + ", connection " + _id.ToString());
          this.DisconnectFromServer(true);
        }
      }
      finally
      {
        buffer = null;
      }
    }
    #endregion

    #region Raw data sending and receiving
    /// <summary>
    /// Asynchronously sends a raw packet, calling the specified method when finished
    /// </summary>
    public void SendPacket(byte[] buffer, AsyncCallback callback)
    {
      if (_readyfordata)
      {
        lock (_sock)
        {
          _sock.BeginSend(buffer, 0, buffer.Length, SocketFlags.None, callback, this);
        }
      }
    }

    /// <summary>
    /// Asynchronously receives a raw packet, calling the specified method when finished
    /// </summary>
    public void ReceivePacket(byte[] buffer, AsyncCallback callback)
    {
      if (_readyfordata)
      {
        _sock.BeginReceive(buffer, 0, buffer.Length, SocketFlags.None, callback, buffer);
      }
    }
    #endregion

    private void SendKeepalive(object threadstate)
    {
      FLAPHeader fh;
      fh.Channel = 0x05;
      fh.DatagramSequenceNumber = 0;
      fh.DataSize = 0;
      byte[] keepalive = new byte[6];
      Marshal.InsertFLAPHeader(fh, keepalive, 0);

      try
      {
        Marshal.InsertUshort(keepalive, (ushort)this.FLAPSequence, 2);
        lock (_sock)
        {
          _sock.Send(keepalive);
        }
        Logging.WriteString(String.Format("Sent keepalive over connection {0}", _id));
      }
      catch (Exception ex)
      {
        if (!_disconnecting)
        {
          string message = "Send error: " + ex.Message;
          Logging.WriteString(message + ", connection " + _id.ToString());
          this.DisconnectFromServer(true);
        }
      }
      finally
      {
        keepalive = null;
      }
      
      _keepalivetimer.Change(60000, Timeout.Infinite);
    }

    /// <summary>
    /// Raises the <see cref="ServerConnectionCompleted"/> event
    /// </summary>
    protected internal void OnServerConnectionCompleted()
    {
      if (this.ServerConnnectionCompleted != null)
      {
        this.ServerConnnectionCompleted(this);
      }
    }

    #region Properties
    /// <summary>
    /// Gets or sets a value indicating whether or not this connection
    /// is in the initial "connecting" phase -- that is, the time between
    /// connection to the server and the client sending SNAC(01,02)
    /// </summary>
    public bool Connecting
    {
      get { return _connecting; }
      set
      {
        if (_connecting != value)
        {
          _connecting = value;
          if (_connecting == false)
          {
            _keepalivetimer.Change(60000, Timeout.Infinite);
          }
        }
      }
    }

    /// <summary>
    /// Gets or sets the server to which to connect
    /// </summary>
    public virtual string Server
    {
      get { return _server; }
      set { _server = value; }
    }

    /// <summary>
    /// Gets or sets the port to which to connect
    /// </summary>
    public virtual int Port
    {
      get { return _port; }
      set { _port = value; }
    }

    /// <summary>
    /// Gets or sets the authentication cookie to send to the server
    /// </summary>
    public byte[] Cookie
    {
      get { return _cookie; }
      set { _cookie = value; }
    }

    /// <summary>
    /// Gets or sets a value indicating whether or not this connection
    /// is ready to send data
    /// </summary>
    public bool ReadyForData
    {
      get { return _readyfordata; }
      set { _readyfordata = true; }
    }

    /// <summary>
    /// Gets the Connection's ID, as assigned by the <see cref="ConnectionManager"/>
    /// </summary>
    public int ID
    {
      get { return _id; }
    }

    /// <summary>
    /// Gets the next FLAP sequence ID in the sequence 0 through 2^15 - 1, inclusive
    /// </summary>
    /// <returns>The next FLAP sequence ID</returns>
    /// <remarks>The sequence ID series wraps around if it is about to overflow</remarks>
    private ushort FLAPSequence
    {
      get
      {
        lock (this)
        {
          ushort retval = _flapsequencenum++;
          if (_flapsequencenum == 0xFFFF)
            _flapsequencenum = 0;
          return retval;
        }
      }
    }

    private ProcessQueue Processor
    {
      get
      {
        return _processor;
      }
    }
#endregion
  }
}
